参考:Ten minute introduction to MobX and React

Mobx 是一种简单,可扩展且经过实战考验的状态管理解决方案。 本教程将在十分钟内教你 MobX 的所有重要概念。 MobX 是一个独立的库,但大多数人都将它与 React 一起使用,本教程重点介绍了这种组合。

核心理念

状态是每个应用程序的核心,许多状态管理解决方案试图限制可以修改状态的方式,例如通过使状态不可变。

MobX 要让状态管理变得简单起来,它解决了根本问题:不允许产生前后不一致的状态。实现这一目标的策略很简单:能从应用的状态(state)中导出的任何派生值(derivation)都是自动导出的。

  1. 首先有一个应用的状态(state),可以是任何 objects,arrays,promitives,references 等等能够构建你的程序的东西。这些值是你的应用程序的元数据(data cells)
  2. 其次是派生值(derivations),可以是任何能自动从你的状态(state)中计算得到的值。
  3. 反应(reaction)derivations很像,不一样的是,reaction 是一个动作,它用于制动执行一些任务,通常是一些 I/O 相关的任务,它们能够确保在合理的时间时,DOM 能够自动更新或者网络请求能够自动执行。
  4. 动作(actions),只有 actions 能够去改变 state,MobX 确保所有 state 的变化都是通过 action 进行,并且会自动地、同步地被传递给 derivation 和 reaction。

一个简单的例子:todo store

下面是一个简单的 TodoStore,维护了一系列待办事项,还没引入 MobX。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class TodoStore {
todos = []

get completedTodosCount() {
return this.todos.filter((todo) => todo.completed === true).length
}

report() {
if (this.todos.length === 0) return "<none>"
return (
`Next todo: "${this.todos[0].task}". ` +
`Progress: ${this.completedTodosCount}/${this.todos.length}`
)
}

addTodo(task) {
this.todos.push({
task: task,
completed: false,
assignee: null,
})
}
}

const todoStore = new TodoStore()

上面我们创建了一个带有 todo 集合的 todoStore 实例。为了确保我们看到更改的效果,我们在每次更改后调用 todoStore.report 并记录它。 请注意,report 有意始终仅打印第一个任务。 它使这个例子有点人为,但正如你将在下面看到的,它很好地证明了 MobX 的依赖性跟踪是动态的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
todoStore.addTodo("read MobX tutorial")
console.log(todoStore.report()) // Next todo: "read MobX tutorial". Progress: 0/1

todoStore.addTodo("try MobX")
console.log(todoStore.report()) // Next todo: "read MobX tutorial". Progress: 0/2

todoStore.todos[0].completed = true
console.log(todoStore.report()) // Next todo: "read MobX tutorial". Progress: 1/2

todoStore.todos[1].task = "try MobX in own project"
console.log(todoStore.report()) // Next todo: "read MobX tutorial". Progress: 1/2

todoStore.todos[0].task = "grok MobX tutorial"
console.log(todoStore.report()) // Next todo: "grok MobX tutorial". Progress: 1/2

console.log(todoStore.completedTodosCount) // 1

Becoming reactive

到目前为止,这段代码并没有什么特别之处。 但是,如果我们不显式地调用 report,我们可以在每次状态更改时自动调用它吗?我们希望确保打印最新的报告,但却不想自己来组织这件事情。

幸运的是,这正是 MobX 可以做的,它可以自动执行依赖于 state 的代码。这样我们的 report 就会自动更新,就像电子表格中的图表一样。为了实现这一点,TodoStore 必须变得可观察,以便 MobX 可以跟踪正在进行的所有更改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class ObservableTodoStore {
@observable todos = []
@observable pendingRequests = 0

constructor() {
mobx.autorun(() => console.log(this.report))
}

@computed get completedTodosCount() {
return this.todos.filter((todo) => todo.completed === true).length
}

@computed get report() {
if (this.todos.length === 0) return "<none>"
return (
`Next todo: "${this.todos[0].task}". ` +
`Progress: ${this.completedTodosCount}/${this.todos.length}`
)
}

addTodo(task) {
this.todos.push({
task: task,
completed: false,
assignee: null,
})
}
}

const observableTodoStore = new ObservableTodoStore()

我们将一些属性标记为@observable,以便在这些值发生变化时通知 MobX。用@computed 修饰计算过程,以标记这些计算过程是从状态派生而来。

到此为止,我们还没用过pendingRequestsassignee。在构造函数中,我们创建了一个小函数,它自动打印报告。 由于 report 依赖于已被修饰为 observable 的 todos 状态,因此它会及时打印报告:

1
2
3
4
5
observableTodoStore.addTodo("read MobX tutorial") // Next todo: "read MobX tutorial". Progress: 0/1
observableTodoStore.addTodo("try MobX") // Next todo: "read MobX tutorial". Progress: 0/2
observableTodoStore.todos[0].completed = true // Next todo: "read MobX tutorial". Progress: 1/2
observableTodoStore.todos[1].task = "try MobX in own project"
observableTodoStore.todos[0].task = "grok MobX tutorial" // Next todo: "grok MobX tutorial". Progress: 1/2

我们可以看到,report这个 reaction,依赖于this.todos.length, this.todos[0].task, this.completedTodosCount,只有当这三个值发生改变时,才会自动调用report。而我们看到上面的第四行代码,其只改变了todos[1].task,并没有改变report所依赖的任何 state 或 reaction,故而没有调用report

Making React reactive

mobx-react 能够使得 React 的组件自动进行渲染,从而使得组件与其依赖的状态能够同步。

下面的代码定义了一些 React 组件,唯一不同的地方就是用了 MobX 的@observer 装饰器,它使得每个组件能够在其相关的数据改变的时候重新渲染。这时我们不再需要setState,也不需要进行状态提升之类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
@observer
class TodoList extends React.Component {
render() {
const store = this.props.store
return (
<div>
{store.report}
<ul>
{store.todos.map((todo, idx) => (
<TodoView todo={todo} key={idx} />
))}
</ul>
{store.pendingRequests > 0 ? (
<marquee>Loading...</marquee>
) : null}
<button onClick={this.onNewTodo}>New Todo</button>
<small> (double-click a todo to edit)</small>
<RenderCounter />
</div>
)
}

onNewTodo = () => {
this.props.store.addTodo(prompt("Enter a new todo:", "coffee plz"))
}
}

@observer
class TodoView extends React.Component {
render() {
const todo = this.props.todo
return (
<li onDoubleClick={this.onRename}>
<input
type="checkbox"
checked={todo.completed}
onChange={this.onToggleCompleted}
/>
{todo.task}
{todo.assignee ? <small>{todo.assignee.name}</small> : null}
<RenderCounter />
</li>
)
}

onToggleCompleted = () => {
const todo = this.props.todo
todo.completed = !todo.completed
}

onRename = () => {
const todo = this.props.todo
todo.task = prompt("Task name", todo.task) || todo.task
}
}

ReactDOM.render(
<TodoList store={observableTodoStore} />,
document.getElementById("reactjs-app")
)

我们看到,TodoList组件依赖于前面的observableTodoStore.todos, observableTodoStore.pendingRequests,故而当observableTodoStore中的todos, pendingRequests发生任何改变时,都会引起TodoList的重新渲染。

在运行下面的代码时,回顾一下上面observableTodoStore的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
observableTodoStore.todos = [
{
task: "grok MobX tutorial",
completed: true,
assignee: null,
},
{
task: "try MobX in own project",
complted: false,
assignee: null,
},
]

observableTodooStore.pendingRequests = 0

我们来运行一下下面的代码:

1
2
3
4
5
const store = observableTodoStore
store.todos[0].completed = !store.todos[0].completed
store.todos[1].task = "Random todo " + Math.random()
store.todos.push({ task: "Find a fine cheese", completed: true })
// etc etc.. add your own statements here...

一开始,初始界面是:

当运行完第二行时:

第三行:

第四行:

Working with references

在之前,所有可观察的对象,都是原始类型的值:字符串,boolean 值,数值等。但当我们依赖的状态只是一个对其他对象的引用的话,该如何?下面的代码展示了该如何监听一个对象。

1
2
3
4
var peopleStore = mobx.observable([{ name: "Michel" }, { name: "Me" }])
observableTodoStore.todos[0].assignee = peopleStore[0]
observableTodoStore.todos[1].assignee = peopleStore[1]
peopleStore[0].name = "Michel Weststrate"